using Geometry;
using Gltf;
using UnityEngine;
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Mathematics;
using Newtonsoft.Json.Linq;
using Cesium3DTilesSelection;
using EarthUtility;
using Maki;
using TriangleClipVertex = Maki.Variant<int, Geometry.InterpolatedVertex>;
using EarthGeospatial;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Reinterpret.Net;

public class upsampleGltf
{
    public struct EdgeVertex
    {
        public int index;
        public float2 uv;
    }

    public class EdgeIndices
    {
        public List<EdgeVertex> west;
        public List<EdgeVertex> south;
        public List<EdgeVertex> east;
        public List<EdgeVertex> north;

        public EdgeIndices()
        {
            west = new List<EdgeVertex>();
            south = new List<EdgeVertex>();
            east = new List<EdgeVertex>();
            north = new List<EdgeVertex>();
        }
    };

    public struct FloatVertexAttribute
    {
        public byte[] buffer;
        public Int64 offset;
        public Int64 stride;
        public Int64 numberOfFloatsPerVertex;
        public Int64 accessorIndex;
        public List<double> minimums;
        public List<double> maximums;
    };



    static unsafe void copyVertexAttributes(
       List<FloatVertexAttribute> vertexAttributes,
        TriangleClipVertex vertex,
        List<float> output,
        bool skipMinMaxUpdate = false)
    {

        Action<int> m1 = (int vertexIndex) =>
        {
            foreach (FloatVertexAttribute attribute in vertexAttributes)
            {

                // float* pInput = reinterpret_cast <const float*> (attribute.buffer.data() + attribute.offset + attribute.stride * vertexIndex);
                fixed (byte* readPtr = attribute.buffer)
                {
                    float* pInput = (float*)(readPtr + attribute.offset + attribute.stride * vertexIndex);
                    for (int i = 0; i < attribute.numberOfFloatsPerVertex; ++i)
                    {
                        float value = *pInput;
                        output.Add(value);
                        if (!skipMinMaxUpdate)
                        {
                            attribute.minimums[(i)] = math.min(attribute.minimums[(i)], (value));
                            attribute.maximums[(i)] = math.max(attribute.maximums[(i)], (value));
                        }
                        ++pInput;
                    }
                }
            }
        };

        Action<InterpolatedVertex> m2 = (InterpolatedVertex vertex) =>
        {
            foreach (FloatVertexAttribute attribute in vertexAttributes)
            {
                fixed (byte* bufferPtr = attribute.buffer)
                {
                    float* pInput0 = (float*)(bufferPtr + attribute.offset + attribute.stride * vertex.first);
                    float* pInput1 = (float*)(bufferPtr + attribute.offset + attribute.stride * vertex.second);

                    for (int i = 0; i < attribute.numberOfFloatsPerVertex; ++i)
                    {
                        float value = math.lerp(*pInput0, *pInput1, (float)vertex.t);
                        output.Add(value);
                        if (!skipMinMaxUpdate)
                        {
                            attribute.minimums[(i)] = math.min(attribute.minimums[(i)], (value));
                            attribute.maximums[(i)] = math.max(attribute.maximums[(i)], (value));
                        }
                        ++pInput0;
                        ++pInput1;
                    }
                }
            }
        };
        vertex.visitAction(m1, m2);
    }

    static void copyVertexAttributes(
        List<FloatVertexAttribute> vertexAttributes,
       List<TriangleClipVertex> complements,
        TriangleClipVertex vertex,
        List<float> output)
    {

        Action<int> m1 = (int vertexIndex) =>
        {
            if (vertexIndex < 0)
            {
                copyVertexAttributes(
                    vertexAttributes,
                    complements[(~vertexIndex)],
                    output);
            }
            else
            {
                copyVertexAttributes(vertexAttributes, vertexIndex, output);
            }
        };

        Action<InterpolatedVertex> m2 = (InterpolatedVertex v) =>
        {
            int outputIndex0 = output.Count;

            // Copy the two vertices into the output array
            if (v.first < 0)
            {
                copyVertexAttributes(
                    vertexAttributes,
                    complements[(~v.first)],
                    output,
                    true);
            }
            else
            {
                copyVertexAttributes(vertexAttributes, v.first, output, true);
            }

            int outputIndex1 = output.Count;

            if (v.second < 0)
            {
                copyVertexAttributes(
                    vertexAttributes,
                    complements[(~v.second)],
                    output,
                    true);
            }
            else
            {
                copyVertexAttributes(vertexAttributes, v.second, output, true);
            }

            // Interpolate between them and overwrite the first with the result.
            foreach (FloatVertexAttribute attribute in vertexAttributes)
            {
                for (int i = 0; i < attribute.numberOfFloatsPerVertex; ++i)
                {
                    float value = math.lerp(output[outputIndex0], output[outputIndex1], ((float)v.t));
                    output[outputIndex0] = value;
                    attribute.minimums[i] = math.min(attribute.minimums[(i)], (value));
                    attribute.maximums[(i)] = math.max(attribute.maximums[(i)], (value));
                    ++outputIndex0;
                    ++outputIndex1;
                }
            }

            // Remove the temporary second, which is now pointed to be outputIndex0.
            // output.erase(output.begin() +static_cast<std::vector<float>::iterator::difference_type>(outputIndex0),output.end());
            output.RemoveAt(outputIndex0);
        };

        vertex.visitAction(m1, m2);
    }


    static float2 getVertexValue(
         AccessorView<float2> accessor,
        TriangleClipVertex vertex)
    {

        Func<int, float2> m1 = (int vertexIndex) => { return accessor[vertexIndex]; };

        Func<InterpolatedVertex, float2> m2 = (InterpolatedVertex v) =>
        {
            float2 v0 = accessor[v.first];
            float2 v1 = accessor[v.second];
            return math.lerp(v0, v1, (float)v.t);
        };

        return vertex.visitFunc(m1, m2);
    }

    static float2 getVertexValue(
     AccessorView<float2> accessor,
     List<TriangleClipVertex> complements,
     TriangleClipVertex vertex)
    {
        Func<int, float2> m1 = (int vertexIndex) =>
        {
            if (vertexIndex < 0)
            {
                return getVertexValue(
                    accessor,
                    complements,
                    complements[(~vertexIndex)]);
            }

            return accessor[vertexIndex];
        };

        Func<InterpolatedVertex, float2> m2 = (InterpolatedVertex vertex) =>
        {
            float2 v0;
            if (vertex.first < 0)
            {
                v0 = getVertexValue(
                    accessor,
                    complements,
                    complements[(~vertex.first)]);
            }
            else
            {
                v0 = accessor[vertex.first];
            }

            float2 v1;
            if (vertex.second < 0)
            {
                v1 = getVertexValue(
                    accessor,
                    complements,
                    complements[(~vertex.second)]);
            }
            else
            {
                v1 = accessor[vertex.second];
            }

            return math.lerp(v0, v1, (float)vertex.t);
        };
        return vertex.visitFunc(m1, m2);

    }
    public static Model upsampleGltfForRasterOverlays(Model parentModel, UpsampledQuadtreeNode childID)
    {
        Model result = new Model();

        // Copy the entire parent model except for the buffers, bufferViews, and
        // accessors, which we'll be rewriting.
        result.animations = parentModel.animations;
        result.materials = parentModel.materials;
        result.meshes = parentModel.meshes;
        result.nodes = parentModel.nodes;
        result.textures = parentModel.textures;
        result.images = parentModel.images;
        result.skins = parentModel.skins;
        result.samplers = parentModel.samplers;
        result.cameras = parentModel.cameras;
        result.scenes = parentModel.scenes;
        result.scene = parentModel.scene;
        result.extensionsUsed = parentModel.extensionsUsed;
        result.extensionsRequired = parentModel.extensionsRequired;
        result.asset = parentModel.asset;
        result.extras = parentModel.extras;
        // result.extensions = parentModel.extensions;
        // result.extras_json_string = parentModel.extras_json_string;
        // result.extensions_json_string = parentModel.extensions_json_string;

        // If the glTF has a name, update it with upsample info.
        var nameIt = result.extras["Cesium3DTiles_TileUrl"];
        if (nameIt != null)
        {
            string name = ((string)nameIt);
            var upsampledIndex = name.IndexOf(" upsampled");
            if (upsampledIndex >= 0)
            {
                name = name.Substring(0, upsampledIndex);
            }
            // string name = "";
            // name += " upsampled L" + childID.tileID.level;
            name += "-X" + childID.tileID.x;
            name += "-Y" + childID.tileID.y;
            result.extras["Cesium3DTiles_TileUrl"] = name;
        }

        foreach (Gltf.Mesh mesh in result.meshes)
        {
            foreach (MeshPrimitive primitive in mesh.primitives)
            {
                upsamplePrimitiveForRasterOverlays(
                    parentModel,
                    result,
                    mesh,
                    primitive,
                    childID);
            }
        }

        return result;
    }

    private static void upsamplePrimitiveForRasterOverlays(Model parentModel, Model model, Gltf.Mesh mesh, MeshPrimitive primitive, UpsampledQuadtreeNode childID)
    {
        if (primitive.mode != MeshPrimitive.Mode.TRIANGLES ||
      primitive.indices < 0 ||
      primitive.indices >= parentModel.accessors.Count)
        {
            // Not indexed triangles, so we don't know how to divide this primitive
            // (yet). So just copy it verbatim.
            // TODO
            return;
        }

        Accessor indicesAccessorGltf = parentModel.accessors[(primitive.indices)];
        if (indicesAccessorGltf.componentType == Accessor.ComponentType.UNSIGNED_SHORT)
        {
            upsampleGltf.upsamplePrimitiveForRasterOverlays<UInt16>(
                  parentModel,
                  model,
                  mesh,
                  primitive,
                  childID);
        }
        else if (indicesAccessorGltf.componentType == Accessor.ComponentType.UNSIGNED_INT)
        {
            upsampleGltf.upsamplePrimitiveForRasterOverlays<UInt32>(
                 parentModel,
                 model,
                 mesh,
                 primitive,
                 childID);
        }
    }
    public static unsafe void upsamplePrimitiveForRasterOverlays<TIndex>(
        Model parentModel, Model model, Gltf.Mesh mesh, MeshPrimitive
         primitive, UpsampledQuadtreeNode childID) where TIndex : unmanaged
    {
        Debug.Log("upsamplePrimitiveForRasterOverlays");

        // Add up the per-vertex size of all attributes and create buffers,
        // bufferViews, and accessors
        List<FloatVertexAttribute> attributes = new List<FloatVertexAttribute>(primitive.attributes.Count);

        int vertexBufferIndex = model.buffers.Count;
        model.buffers.Add(new Gltf.Buffer());

        int indexBufferIndex = model.buffers.Count;
        model.buffers.Add(new Gltf.Buffer());

        int vertexBufferViewIndex = model.bufferViews.Count;
        model.bufferViews.Add(new BufferView());

        int indexBufferViewIndex = model.bufferViews.Count;
        model.bufferViews.Add(new BufferView());

        BufferView vertexBufferView = model.bufferViews[vertexBufferViewIndex];
        vertexBufferView.buffer = (int)(vertexBufferIndex);
        vertexBufferView.target = BufferView.Target.ARRAY_BUFFER;

        BufferView indexBufferView = model.bufferViews[indexBufferViewIndex];
        indexBufferView.buffer = (int)(indexBufferIndex);
        indexBufferView.target = BufferView.Target.ARRAY_BUFFER;

        Int64 vertexSizeFloats = 0;
        int uvAccessorIndex = -1;
        int positionAttributeIndex = -1;

        List<string> toRemove = new List<string>();
        for (int i = 0; i < primitive.attributes.Count; i++)
        {
            var attribute = primitive.attributes.ElementAt(i);

            if (attribute.Key.Contains("_CESIUMOVERLAY_"))
            {
                if (uvAccessorIndex == -1)
                {
                    uvAccessorIndex = attribute.Value;
                }
                // Do not include _CESIUMOVERLAY_*, it will be generated later.
                toRemove.Add(attribute.Key);
                continue;
            }

            if (attribute.Value < 0 || attribute.Value >= (parentModel.accessors.Count))
            {
                toRemove.Add(attribute.Key);
                continue;
            }

            Accessor accessor = parentModel.accessors[(int)(attribute.Value)];
            if (accessor.bufferView < 0 || accessor.bufferView >= (parentModel.bufferViews.Count))
            {
                toRemove.Add(attribute.Key);
                continue;
            }

            BufferView bufferView = parentModel.bufferViews[(accessor.bufferView)];
            if (bufferView.buffer < 0 || bufferView.buffer >= (parentModel.buffers.Count))
            {
                toRemove.Add(attribute.Key);
                continue;
            }

            Gltf.Buffer buffer = parentModel.buffers[(bufferView.buffer)];

            Int64 accessorByteStride = accessor.computeByteStride(parentModel);
            Int64 accessorComponentElements = accessor.computeNumberOfComponents();
            if (accessor.componentType != Accessor.ComponentType.FLOAT)
            {
                // Can only interpolate floating point vertex attributes
                return;
            }
            primitive.attributes[attribute.Key] = model.accessors.Count;

            Accessor newAccessor = new Accessor();
            model.accessors.Add(newAccessor);
            newAccessor.bufferView = (vertexBufferIndex);
            newAccessor.byteOffset = vertexSizeFloats * (sizeof(float));
            newAccessor.componentType = Accessor.ComponentType.FLOAT;
            newAccessor.type = accessor.type;
            vertexSizeFloats += accessorComponentElements;

            var vertexAttribute = new FloatVertexAttribute()
            {
                buffer = buffer.cesium.data,
                offset = accessor.byteOffset,
                stride = accessorByteStride,
                numberOfFloatsPerVertex = accessorComponentElements,
                accessorIndex = attribute.Value,
                minimums = Enumerable.Repeat(double.MaxValue, (int)accessorComponentElements).ToList(),
                maximums = Enumerable.Repeat(double.MinValue, (int)accessorComponentElements).ToList(),
            };
            attributes.Add(vertexAttribute);

            // get position to be used to create for skirts later
            if (attribute.Key == "POSITION")
            {
                positionAttributeIndex = (attributes.Count - 1);
            }
        }

        if (uvAccessorIndex == -1)
        {
            // We don't know how to divide this primitive, so just copy it verbatim.
            // TODO
            return;
        }

        foreach (string attribute in toRemove)
        {
            primitive.attributes.Remove(attribute);
        }

        bool keepAboveU = !isWestChild(childID);
        bool keepAboveV = !isSouthChild(childID);

        AccessorView<float2> uvView = new AccessorView<float2>(parentModel, uvAccessorIndex);
        AccessorView<TIndex> indicesView = new AccessorView<TIndex>(parentModel, primitive.indices);

        if (uvView.status() != AccessorViewStatus.Valid ||
            indicesView.status() != AccessorViewStatus.Valid)
        {
            return;
        }

        // check if the primitive has skirts
        Int64 indicesBegin = 0;
        Int64 indicesCount = indicesView.size();
        SkirtMeshMetadata parentSkirtMeshMetadata = SkirtMeshMetadata.parseFromGltfExtras(primitive.extras);
        bool hasSkirt = (parentSkirtMeshMetadata != null) && (positionAttributeIndex != -1);
        if (hasSkirt)
        {
            indicesBegin = parentSkirtMeshMetadata.noSkirtIndicesBegin;
            indicesCount = parentSkirtMeshMetadata.noSkirtIndicesCount;
        }

        List<int> clipVertexToIndices = new List<int>();
        List<TriangleClipVertex> clippedA = new List<TriangleClipVertex>();
        List<TriangleClipVertex> clippedB = new List<TriangleClipVertex>();

        // Maps old (parentModel) vertex indices to new (model) vertex indices.
        var vertexMap = Enumerable.Repeat(int.MaxValue, uvView.size()).ToList();

        // List<unsigned char> newVertexBuffer(vertexSizeFloats *
        // sizeof(float)); gsl.span<float>
        // newVertexFloats(reinterpret_cast<float*>(newVertexBuffer.data()),
        // newVertexBuffer.size() / sizeof(float));
        List<float> newVertexFloats = new List<float>();
        List<int> indices = new List<int>();


        EdgeIndices edgeIndices = new EdgeIndices();

        for (Int64 i = indicesBegin; i < indicesBegin + indicesCount; i += 3)
        {
            int i0 = (int)System.Convert.ChangeType(indicesView[i], TypeCode.Int32);
            int i1 = (int)System.Convert.ChangeType(indicesView[i + 1], TypeCode.Int32);
            int i2 = (int)System.Convert.ChangeType(indicesView[i + 2], TypeCode.Int32);

            float2 uv0 = uvView[i0];
            float2 uv1 = uvView[i1];
            float2 uv2 = uvView[i2];

            // Clip this triangle against the East-West boundary
            clippedA.Clear();
            clipTriangle.clipTriangleAtAxisAlignedThreshold(
                 0.5,
                 keepAboveU,
                 (i0),
                 (i1),
                 (i2),
                 uv0.x,
                 uv1.x,
                 uv2.x,
                 clippedA);

            if (clippedA.Count < 3)
            {
                // No part of this triangle is inside the target tile.
                continue;
            }

            // Clip the first clipped triange against the North-South boundary
            clipVertexToIndices.Clear();
            clippedB.Clear();
            clipTriangle.clipTriangleAtAxisAlignedThreshold(
                0.5,
                keepAboveV,
                ~0,
                ~1,
                ~2,
                getVertexValue(uvView, clippedA[0]).y,
                getVertexValue(uvView, clippedA[1]).y,
                getVertexValue(uvView, clippedA[2]).y,
                clippedB);

            // Add the clipped triangle or quad, if any
            addClippedPolygon(
                newVertexFloats,
                indices,
                attributes,
                vertexMap,
                clipVertexToIndices,
                clippedA,
                clippedB);
            if (hasSkirt)
            {
                addEdge(
                     edgeIndices,
                    0.5,
                    0.5,
                    keepAboveU,
                    keepAboveV,
                    uvView,
                    clipVertexToIndices,
                    clippedA,
                    clippedB);
            }

            // If the East-West clip yielded a quad (rather than a triangle), clip the
            // second triangle of the quad, too.
            if (clippedA.Count > 3)
            {
                clipVertexToIndices.Clear();
                clippedB.Clear();
                clipTriangle.clipTriangleAtAxisAlignedThreshold(
                    0.5,
                    keepAboveV,
                    ~0,
                    ~2,
                    ~3,
                    getVertexValue(uvView, clippedA[0]).y,
                    getVertexValue(uvView, clippedA[2]).y,
                    getVertexValue(uvView, clippedA[3]).y,
                    clippedB);

                // Add the clipped triangle or quad, if any
                addClippedPolygon(
                    newVertexFloats,
                    indices,
                    attributes,
                    vertexMap,
                    clipVertexToIndices,
                    clippedA,
                    clippedB);
                if (hasSkirt)
                {
                    addEdge(
                       edgeIndices,
                        0.5,
                        0.5,
                        keepAboveU,
                        keepAboveV,
                        uvView,
                        clipVertexToIndices,
                        clippedA,
                        clippedB);
                }
            }
        }

        // create mesh with skirt
        SkirtMeshMetadata skirtMeshMetadata = null;
        if (hasSkirt)
        {
            skirtMeshMetadata = new SkirtMeshMetadata();
            skirtMeshMetadata.noSkirtIndicesBegin = 0;
            skirtMeshMetadata.noSkirtIndicesCount = indices.Count;
            skirtMeshMetadata.meshCenter = parentSkirtMeshMetadata.meshCenter;
            addSkirts(
                newVertexFloats,
                indices,
                attributes,
                childID,
                skirtMeshMetadata,
                parentSkirtMeshMetadata,
                edgeIndices,
                vertexSizeFloats,
                positionAttributeIndex);
        }

        // Update the accessor vertex counts and min/max values
        Int64 numberOfVertices = (Int64)(newVertexFloats.Count) / vertexSizeFloats;
        foreach (FloatVertexAttribute attribute in attributes)
        {
            Accessor accessor = model.accessors[((int)attribute.accessorIndex)];
            accessor.count = numberOfVertices;
            accessor.min = attribute.minimums;
            accessor.max = attribute.maximums;
        }

        // Add an accessor for the indices
        int indexAccessorIndex = model.accessors.Count;
        Accessor newIndicesAccessor = new Accessor();
        model.accessors.Add(newIndicesAccessor);

        newIndicesAccessor.bufferView = (indexBufferViewIndex);
        newIndicesAccessor.byteOffset = 0;
        newIndicesAccessor.count = (indices.Count);
        newIndicesAccessor.componentType = Accessor.ComponentType.UNSIGNED_INT;
        newIndicesAccessor.type = Accessor.Type.SCALAR;

        // Populate the buffers
        Gltf.Buffer vertexBuffer = model.buffers[vertexBufferIndex];
        vertexBuffer.cesium.data = newVertexFloats.ToArray().Reinterpret();
        vertexBufferView.byteLength = (vertexBuffer.cesium.data.Length);
        vertexBufferView.byteStride = vertexSizeFloats * (sizeof(float));

        Gltf.Buffer indexBuffer = model.buffers[indexBufferIndex];
        indexBuffer.cesium.data = indices.ToArray().Reinterpret();
        indexBufferView.byteLength = (indexBuffer.cesium.data.Length);

        bool onlyWater = false;
        bool onlyLand = true;
        int waterMaskTextureId = -1;

        var onlyWaterIt = primitive.extras["OnlyWater"];
        var onlyLandIt = primitive.extras["OnlyLand"];

        if (onlyWaterIt != null && onlyWaterIt.Type == JTokenType.Boolean &&
            onlyLandIt != null && onlyLandIt.Type == JTokenType.Boolean)
        {

            onlyWater = ((bool)onlyWaterIt);
            onlyLand = ((bool)onlyLandIt);

            if (!onlyWater && !onlyLand)
            {
                // We have to use the parent's water mask
                var waterMaskTextureIdIt = primitive.extras["WaterMaskTex"];
                if (waterMaskTextureIdIt != null && waterMaskTextureIdIt.Type == JTokenType.Integer)
                {
                    waterMaskTextureId = ((int)waterMaskTextureIdIt);
                }
            }
        }

        double waterMaskTranslationX = 0.0;
        double waterMaskTranslationY = 0.0;
        double waterMaskScale = 0.0;

        var waterMaskTranslationXIt = primitive.extras["WaterMaskTranslationX"];
        var waterMaskTranslationYIt = primitive.extras["WaterMaskTranslationY"];
        var waterMaskScaleIt = primitive.extras["WaterMaskScale"];

        if (waterMaskTranslationXIt != null &&
            waterMaskTranslationXIt.Type == JTokenType.Float &&
            waterMaskTranslationYIt != null &&
            waterMaskTranslationYIt.Type == JTokenType.Float &&
            waterMaskScaleIt != null &&
            waterMaskScaleIt.Type == JTokenType.Float)
        {
            waterMaskScale = 0.5 * ((double)waterMaskScaleIt);
            waterMaskTranslationX = ((double)waterMaskTranslationXIt) + waterMaskScale * (childID.tileID.x % 2);
            waterMaskTranslationY = ((double)waterMaskTranslationYIt) + waterMaskScale * (childID.tileID.y % 2);
        }

        // add skirts to extras to be upsampled later if needed
        if (hasSkirt)
        {
            primitive.extras = SkirtMeshMetadata.createGltfExtras(skirtMeshMetadata);
        }

        primitive.extras.Add("OnlyWater", onlyWater);
        primitive.extras.Add("OnlyLand", onlyLand);

        primitive.extras.Add("WaterMaskTex", waterMaskTextureId);

        primitive.extras.Add("WaterMaskTranslationX", waterMaskTranslationX);
        primitive.extras.Add("WaterMaskTranslationY", waterMaskTranslationY);
        primitive.extras.Add("WaterMaskScale", waterMaskScale);
        primitive.indices = (indexAccessorIndex);
    }

    private static bool isSouthChild(UpsampledQuadtreeNode childID)
    {
        return (childID.tileID.x % 2) == 0;
    }

    private static bool isWestChild(UpsampledQuadtreeNode childID)
    {
        return (childID.tileID.y % 2) == 0;
    }

    public static int getOrCreateVertex(
        List<float> output,
        List<FloatVertexAttribute> attributes,
        List<int> vertexMap,
        List<TriangleClipVertex> complements,
         TriangleClipVertex clipVertex)
    {
        var isIndex = clipVertex.Is<int>();
        if (isIndex)
        {
            var pIndex = clipVertex.Get<int>();
            if (pIndex < 0)
            {
                return getOrCreateVertex(
                    output,
                    attributes,
                    vertexMap,
                    complements,
                    complements[(~(pIndex))]);
            }

            int existingIndex = vertexMap[pIndex];
            if (existingIndex != int.MaxValue)
            {
                return ((int)existingIndex);
            }
        }

        int beforeOutput = output.Count;
        copyVertexAttributes(attributes, complements, clipVertex, output);
        int newIndex = beforeOutput / ((output.Count) - beforeOutput);

        if (isIndex)
        {
            var pIndex = clipVertex.Get<int>();
            if (pIndex >= 0)
                vertexMap[(pIndex)] = newIndex;
        }

        return newIndex;
    }

    static void addClippedPolygon(
    List<float> output,
    List<int> indices,
    List<FloatVertexAttribute> attributes,
    List<int> vertexMap,
    List<int> clipVertexToIndices,
    List<TriangleClipVertex> complements,
    List<TriangleClipVertex> clipResult)
    {
        if (clipResult.Count < 3)
        {
            return;
        }

        int i0 = getOrCreateVertex(
           output,
           attributes,
           vertexMap,
           complements,
           clipResult[0]);
        int i1 = getOrCreateVertex(
           output,
           attributes,
           vertexMap,
           complements,
           clipResult[1]);
        int i2 = getOrCreateVertex(
           output,
           attributes,
           vertexMap,
           complements,
           clipResult[2]);

        indices.Add(i0);
        indices.Add(i1);
        indices.Add(i2);

        clipVertexToIndices.Add(i0);
        clipVertexToIndices.Add(i1);
        clipVertexToIndices.Add(i2);

        if (clipResult.Count > 3)
        {
            int i3 = getOrCreateVertex(
               output,
               attributes,
               vertexMap,
               complements,
               clipResult[3]);

            indices.Add(i0);
            indices.Add(i2);
            indices.Add(i3);

            clipVertexToIndices.Add(i3);
        }
    }

    static void addEdge(
        EdgeIndices edgeIndices,
        double thresholdU,
        double thresholdV,
        bool keepAboveU,
        bool keepAboveV,
    AccessorView<float2> uvs,
    List<int> clipVertexToIndices,
    List<TriangleClipVertex> complements,
    List<TriangleClipVertex> clipResult)
    {
        for (int i = 0; i < clipVertexToIndices.Count; ++i)
        {
            float2 uv = getVertexValue(uvs, complements, clipResult[i]);

            if (Mathd.equalsEpsilon(
                    uv.x,
                    0.0,
                    Mathd.EPSILON4))
            {
                edgeIndices.west.Add(new EdgeVertex()
                {
                    index = clipVertexToIndices[i],
                    uv = uv
                });
            }

            if (Mathd.equalsEpsilon(
                    uv.x,
                    1.0,
                    Mathd.EPSILON4))
            {
                edgeIndices.east.Add(new EdgeVertex()
                {
                    index = clipVertexToIndices[i],
                    uv = uv
                });
            }

            if (Mathd.equalsEpsilon(
                    uv.x,
                    thresholdU,
                    Mathd.EPSILON4))
            {
                if (keepAboveU)
                {
                    edgeIndices.west.Add(new EdgeVertex()
                    {
                        index = clipVertexToIndices[i],
                        uv = uv
                    });
                }
                else
                {
                    edgeIndices.east.Add(new EdgeVertex()
                    {
                        index = clipVertexToIndices[i],
                        uv = uv
                    });
                }
            }

            if (Mathd.equalsEpsilon(
                    uv.y,
                    0.0,
                    Mathd.EPSILON4))
            {
                edgeIndices.south.Add(new EdgeVertex()
                {
                    index = clipVertexToIndices[i],
                    uv = uv
                });
            }

            if (Mathd.equalsEpsilon(
                    uv.y,
                    1.0,
                    Mathd.EPSILON4))
            {
                edgeIndices.north.Add(new EdgeVertex()
                {
                    index = clipVertexToIndices[i],
                    uv = uv
                });
            }

            if (Mathd.equalsEpsilon(
                    uv.y,
                    thresholdV,
                    Mathd.EPSILON4))
            {
                if (keepAboveV)
                {
                    edgeIndices.south.Add(new EdgeVertex()
                    {
                        index = clipVertexToIndices[i],
                        uv = uv
                    });
                }
                else
                {
                    edgeIndices.north.Add(new EdgeVertex()
                    {
                        index = clipVertexToIndices[i],
                        uv = uv
                    });
                }
            }
        }
    }

    static void addSkirt(
        List<float> output,
        List<int> indices,
        List<FloatVertexAttribute> attributes,
     List<int> edgeIndices,
     double3 center,
    double skirtHeight,
    Int64 vertexSizeFloats,
    int positionAttributeIndex)
    {
        Ellipsoid ellipsoid = Ellipsoid.WGS84;

        int newEdgeIndex = ((int)(output.Count / (vertexSizeFloats)));
        for (int i = 0; i < edgeIndices.Count; ++i)
        {
            int edgeIdx = edgeIndices[i];
            int offset = 0;
            for (int j = 0; j < attributes.Count; ++j)
            {
                FloatVertexAttribute attribute = attributes[j];
                int valueIndex = offset + ((int)(vertexSizeFloats)) * edgeIdx;

                if (j == positionAttributeIndex)
                {
                    double3 position = new double3 { x = output[valueIndex], y = output[valueIndex + 1], z = output[valueIndex + 2] };
                    position += center;
                    position -= skirtHeight * ellipsoid.geodeticSurfaceNormal(position);
                    position -= center;

                    for (int c = 0; c < 3; ++c)
                    {
                        output.Add((float)(position[(c)]));
                        attribute.minimums[c] = math.min(
                            attribute.minimums[c],
                            position[(c)]);
                        attribute.maximums[c] = math.max(
                            attribute.maximums[c],
                            position[(c)]);
                    }
                }
                else
                {
                    for (int c = 0; c < (attribute.numberOfFloatsPerVertex); ++c)
                    {
                        output.Add((float)output[valueIndex + c]);
                        attribute.minimums[c] = math.min(
                            attribute.minimums[c], (output.Last()));
                        attribute.maximums[c] = math.max(
                            attribute.maximums[c],
                            (output.Last()));
                    }
                }

                offset += (int)(attribute.numberOfFloatsPerVertex);
            }

            if (i < edgeIndices.Count() - 1)
            {
                int nextEdgeIdx = edgeIndices[i + 1];
                indices.Add(edgeIdx);
                indices.Add(nextEdgeIdx);
                indices.Add(newEdgeIndex);

                indices.Add(newEdgeIndex);
                indices.Add(nextEdgeIdx);
                indices.Add(newEdgeIndex + 1);
            }

            ++newEdgeIndex;
        }
    }

    static void addSkirts(
        List<float> output,
        List<int> indices,
        List<FloatVertexAttribute> attributes,
        UpsampledQuadtreeNode childID,
        SkirtMeshMetadata currentSkirt,
         SkirtMeshMetadata parentSkirt,
        EdgeIndices edgeIndices,
        Int64 vertexSizeFloats,
        int positionAttributeIndex)
    {
        Debug.Log("addSkirts");

        double3 center = currentSkirt.meshCenter;
        double shortestSkirtHeight = math.min(parentSkirt.skirtWestHeight, parentSkirt.skirtEastHeight);
        shortestSkirtHeight = math.min(shortestSkirtHeight, parentSkirt.skirtSouthHeight);
        shortestSkirtHeight = math.min(shortestSkirtHeight, parentSkirt.skirtNorthHeight);

        // west
        if (isWestChild(childID))
        {
            currentSkirt.skirtWestHeight = parentSkirt.skirtWestHeight;
        }
        else
        {
            currentSkirt.skirtWestHeight = shortestSkirtHeight * 0.5;
        }

        var sortEdgeIndices = edgeIndices.west.OrderBy(x => x.uv.y).Select(x => x.index).ToList();


        addSkirt(
            output,
            indices,
            attributes,
            sortEdgeIndices,
            center,
            currentSkirt.skirtWestHeight,
            vertexSizeFloats,
            positionAttributeIndex);

        // south
        if (isSouthChild(childID))
        {
            currentSkirt.skirtSouthHeight = parentSkirt.skirtSouthHeight;
        }
        else
        {
            currentSkirt.skirtSouthHeight = shortestSkirtHeight * 0.5;
        }

        sortEdgeIndices = edgeIndices.south.OrderBy(x => x.uv.x).Select(x => x.index).ToList();

        addSkirt(
            output,
            indices,
            attributes,
            sortEdgeIndices,
            center,
            currentSkirt.skirtSouthHeight,
            vertexSizeFloats,
            positionAttributeIndex);

        // east
        if (!isWestChild(childID))
        {
            currentSkirt.skirtEastHeight = parentSkirt.skirtEastHeight;
        }
        else
        {
            currentSkirt.skirtEastHeight = shortestSkirtHeight * 0.5;
        }

        sortEdgeIndices = edgeIndices.east.OrderBy(x => x.uv.y).Select(x => x.index).ToList();
        addSkirt(
            output,
            indices,
            attributes,
            sortEdgeIndices,
            center,
            currentSkirt.skirtEastHeight,
            vertexSizeFloats,
            positionAttributeIndex);

        // north
        if (!isSouthChild(childID))
        {
            currentSkirt.skirtNorthHeight = parentSkirt.skirtNorthHeight;
        }
        else
        {
            currentSkirt.skirtNorthHeight = shortestSkirtHeight * 0.5;
        }

        sortEdgeIndices = edgeIndices.north.OrderBy(x => x.uv.x).Select(x => x.index).ToList();
        addSkirt(
            output,
            indices,
            attributes,
            sortEdgeIndices,
            center,
            currentSkirt.skirtNorthHeight,
            vertexSizeFloats,
            positionAttributeIndex);
    }

}